#compdef firejail

# Documentation: man 1 zshcompsys
# HowTo: https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org

_all_firejails() {
    local -a _all_firejails_list
    for jail in ${(f)"$(_call_program modules_tag "firejail --list 2> /dev/null | cut -d: -f1")"}; do
        _all_firejails_list+=${jail%% *}
    done
    _describe 'firejails list' _all_firejails_list
}

_all_cpus() {
    _cpu_count=$(getconf _NPROCESSORS_ONLN)
    for i in {0..$((_cpu_count-1))} ; do
        print $i
    done
}

_profiles() {
    print $1/*.profile | sed -E "s;$1/;;g;s;\.profile;;g;"
}
_profiles_with_ext() {
    print $1/*.profile
}

_all_profiles() {
    _values 'profiles' $(_profiles @sysconfdir@/firejail) $(_profiles $HOME/.config/firejail) $(_profiles_with_ext .)
}

_session_bus_names() {
    _values names $(busctl --user list --no-legend --activatable | cut -d" " -f1)
    # Alternatives to hack on for non-systemd systems:
    #  dbus-send --session --dest=org.freedesktop.DBus --type=method_call --print-reply=literal /org/freedesktop/DBus org.freedesktop.DBus.ListNames
    #  ls /usr/share/dbus-1/services | xargs -I FILENAME basename FILENAME .service
}

_system_bus_names() {
    _values names $(busctl --system list --no-legend --activatable | cut -d" " -f1)
}

_caps() {
    _values -s "," caps $(firejail --debug-caps | awk '/[0-9]+\s*- /{print $3}')
}

_firejail_args=(
    '*::arguments:_normal'

    '--appimage[sandbox an AppImage application]'
    '--build[build a whitelisted profile for the application and print it on stdout]'
    '--build=-[build a whitelisted profile for the application and save it]: :_files'
    # Ignore that you can do -? too as it's the only short option
    '--help[this help screen]'
    '--join=-[join the sandbox name|pid]: :_all_firejails'
    '--join-filesystem=-[join the mount namespace name|pid]: :_all_firejails'
    '--list[list all sandboxes]'
    '(--profile)--noprofile[do not use a security profile]'
    '(--noprofile)--profile=-[use a custom profile]: :_all_profiles'
    '--shutdown=-[shutdown the sandbox identified by name|pid]: :_all_firejails'
    '--top[monitor the most CPU-intensive sandboxes]'
    '--tree[print a tree of all sandboxed processes]'
    '--version[print program version and exit]'

    '--ids-check[verify file system]'
    '--ids-init[initialize IDS database]'

    '--debug[print sandbox debug messages]'
    '--debug-blacklists[debug blacklisting]'
    '--debug-caps[print all recognized capabilities]'
    '--debug-errnos[print all recognized error numbers]'
    '--debug-private-lib[debug for --private-lib option]'
    '--debug-protocols[print all recognized protocols]'
    '--debug-syscalls[print all recognized system calls]'
    '--debug-syscalls32[print all recognized 32 bit system calls]'
    '--debug-whitelists[debug whitelisting]'

    '--caps.print=-[print the caps filter name|pid]:firejail:_all_firejails'
    '--cpu.print=-[print the cpus in use name|pid]: :_all_firejails'
    '--fs.print=-[print the filesystem log name|pid]: :_all_firejails'
    '--profile.print=-[print the name of profile file name|pid]: :_all_firejails'
    '--protocol.print=-[print the protocol filter name|pid]: :_all_firejails'
    '--seccomp.print=-[print the seccomp filter for the sandbox identified by name|pid]: :_all_firejails'

    '--allow-debuggers[allow tools such as strace and gdb inside the sandbox]'
    '--allusers[all user home directories are visible inside the sandbox]'
    # Should be _files, a comma and files or files -/
    '*--bind=-[mount-bind dirname1/filename1 on top of dirname2/filename2]: :(file1,file2 dir1,dir2)'
    '*--blacklist=-[blacklist directory or file]: :_files'
    '--caps[enable default Linux capabilities filter]'
    '--caps.drop=all[drop all capabilities]'
    '*--caps.drop=-[drop capabilities: all|cap1,cap2,...]: :_caps'
    '*--caps.keep=-[keep capabilities: cap1,cap2,...]: :_caps'
    '--cpu=-[set cpu affinity]: :->cpus'
    "--deterministic-exit-code[always exit with first child's status code]"
    '--deterministic-shutdown[terminate orphan processes]'
    '*--dns=-[set DNS server]: :'
    '*--env=-[set environment variable]: :'
    '--hostname=-[set sandbox hostname]: :'
    '--hosts-file=-[use file as /etc/hosts]: :_files'
    '*--ignore=-[ignore command in profile files]: :'
    '--ipc-namespace[enable a new IPC namespace]'
    '--join-or-start=-[join the sandbox or start a new one name|pid]: :_all_firejails'
    '--keep-config-pulse[disable automatic ~/.config/pulse init]'
    '--keep-dev-ntsync[/dev/ntsync character device is untouched (even with --private-dev)]'
    '--keep-dev-shm[/dev/shm directory is untouched (even with --private-dev)]'
    '--keep-dev-tpm[/dev/tpm* devices are untouched (even with --private-dev)]'
    '--keep-fd[inherit open file descriptors to sandbox]: :'
    '--keep-shell-rc[do not copy shell rc files from /etc/skel]'
    '--keep-var-tmp[/var/tmp directory is untouched]'
#ifdef HAVE_LANDLOCK
    '--landlock.enforce[enforce the Landlock ruleset]'
    '--landlock.fs.read=-[add a read access rule for the path to the Landlock ruleset]: :_files'
    '--landlock.fs.write=-[add a write access rule for the path to the Landlock ruleset]: :_files'
    '--landlock.fs.makeipc=-[add an access rule for the path to the Landlock ruleset for creating named pipes and sockets]: :_files'
    '--landlock.fs.makedev=-[add an access rule for the path to the Landlock ruleset for creating block/char devices]: :_files'
    '--landlock.fs.execute=-[add an execute access rule for the path to the Landlock ruleset]: :_files'
#endif
    '--machine-id[spoof /etc/machine-id with a random id]'
    '--memory-deny-write-execute[seccomp filter to block attempts to create memory mappings that are both writable and executable]'
    '*--mkdir=-[create a directory]:'
    '*--mkfile=-[create a file]:'
    '--name=-[set sandbox name]: :'
    '--net=none[enable a new, unconnected network namespace]'
    # Sample values as I don't think
    # many would enjoy getting a list from -20..20
    '--nice=-[set nice value]: :(1 10 15 20)'
    '--no3d[disable 3D hardware acceleration]'
    '--noblacklist=-[disable blacklist for file or directory]: :_files'
    '--nodbus[disable D-Bus access]'
    '--nodvd[disable DVD and audio CD devices]'
    '*--noexec=-[remount the file or directory noexec nosuid and nodev]: :_files'
    '--nogroups[disable supplementary groups]'
    '--noinput[disable input devices]'
    '--nonewprivs[sets the NO_NEW_PRIVS prctl]'
    '--noprinters[disable printers]'
    '--nosound[disable sound system]'
    '--nou2f[disable U2F devices]'
    '--novideo[disable video devices]'
    '--private[temporary home directory]'
    '--private=-[use directory as user home]: :_files -/'
    '--private-bin=-[build a new /bin in a temporary filesystem, and copy the programs in the list]: :_files -W /usr/bin'
    '--private-cwd[do not inherit working directory inside jail]'
    '--private-cwd=-[set working directory inside jail]: :_files -/'
    '--private-dev[create a new /dev directory with a small number of common device files]'
    '(--writable-etc)--private-etc=-[build a new /etc in a temporary filesystem, and copy the files and directories in the list]: :_files -W /etc'
    '--private-opt=-[build a new /opt in a temporary filesystem]: :_files -W /opt'
    '--private-srv=-[build a new /srv in a temporary filesystem]: :_files -W /srv'
    '--private-tmp[mount a tmpfs on top of /tmp directory]'
    '*--protocol=-[enable protocol filter]: :_values -s , protocols unix inet inet6 netlink packet bluetooth'
    "--quiet[turn off Firejail's output.]"
    '*--read-only=-[set directory or file read-only]: :_files'
    '*--read-write=-[set directory or file read-write]: :_files'
    '--restrict-namespaces[seccomp filter that blocks attempts to create new namespaces]'
    '--restrict-namespaces=-[seccomp filter that blocks attempts to create specified namespaces]: :'
    "--rlimit-as=-[set the maximum size of the process's virtual memory (address space) in bytes]: :"
    '--rlimit-cpu=-[set the maximum CPU time in seconds]: :'
    '--rlimit-fsize=-[set the maximum file size that can be created by a process]: :'
    '--rlimit-nofile=-[set the maximum number of files that can be opened by a process]: :'
    '--rlimit-nproc=-[set the maximum number of processes that can be created for the real user ID of the calling process]: :'
    '--rlimit-sigpending=-[set the maximum number of pending signals for a process]: :'
    '*--rmenv=-[remove environment variable in the new sandbox]: :_values environment-variables $(env | cut -d= -f1)'
    '--seccomp[enable seccomp filter and apply the default blacklist]: :'
    '--seccomp=-[enable seccomp filter, blacklist the default syscall list and the syscalls specified by the command]: :->seccomp'
    '--seccomp.block-secondary[build only the native architecture filters]'
    '*--seccomp.drop=-[enable seccomp filter, and blacklist the syscalls specified by the command]: :->seccomp'
    '*--seccomp.keep=-[enable seccomp filter, and whitelist the syscalls specified by the command]: :->seccomp'
    '*--seccomp.32.drop=-[enable seccomp filter, and blacklist the 32 bit syscalls specified by the command]: :'
    '*--seccomp.32.keep=-[enable seccomp filter, and whitelist the 32 bit syscalls specified by the command]: :'
    # FIXME: Add errnos
    '--seccomp-error-action=-[change error code, kill process or log the attempt]: :(kill log)'
    '--timeout=-[kill the sandbox automatically after the time has elapsed]: :'
    #'(--tracelog)--trace[trace open, access and connect system calls]'
    '(--tracelog)--trace=-[trace open, access and connect system calls]: :_files'
    '(--trace)--tracelog[add a syslog message for every access to files or directories blacklisted by the security profile]'
    '(--private-etc)--writable-etc[/etc directory is mounted read-write]'
    '--tab[enable shell tab completion in sandboxes using private or whitelisted home directories]'
    '--writable-run-user[allow access to /run/user/$UID/systemd and /run/user/$UID/gnupg]'
    '--writable-var[/var directory is mounted read-write]'
    '--writable-var-log[use the real /var/log directory, not a clone]'

#ifdef HAVE_APPARMOR
    '--apparmor[enable AppArmor confinement with the default profile]'
    '--apparmor=-[enable AppArmor confinement with a custom profile]: :'
    '--apparmor.print=-[print apparmor status name|pid]:firejail:_all_firejails'
#endif

#ifdef HAVE_CHROOT
    '(--noroot --overlay --overlay-named --overlay-tmpfs)--chroot=-[chroot into directory]: :_files -/'
#endif

#ifdef HAVE_DBUSPROXY
    # FIXME: _xx_bus_names is actually wrong for --dbus-*.{broadcast,call}.
    # We can steal some function from https://github.com/systemd/systemd/blob/main/shell-completion/zsh/_busctl
    '--dbus-log=-[set DBus log file location]: :_files'
    '--dbus-system=-[set system DBus access policy]: :(filter none)'
    '--dbus-system.broadcast=-[allow signals on the system DBus according to rule]: :_system_bus_names'
    '--dbus-system.call=-[allow calls on the system DBus according to rule]: :_system_bus_names'
    '--dbus-system.own=-[allow ownership of name on the system DBus]: :_system_bus_names'
    '--dbus-system.see=-[allow seeing name on the system DBus]: :_system_bus_names'
    '--dbus-system.talk=-[allow talking to name on the system DBus]: :_system_bus_names'
    '--dbus-user=-[set session DBus access policy or none]: :(filter none)'
    '--dbus-user.broadcast=-[allow signals on the session DBus according to rule]: :_session_bus_names'
    '--dbus-user.call=-[allow calls on the session DBus according to rule]: :_session_bus_names'
    '--dbus-user.own=-[allow ownership of name on the session DBus]: :_session_bus_names'
    '--dbus-user.see=-[allow seeing name on the session DBus]: :_session_bus_names'
    '--dbus-user.talk=-[allow talking to name on the session DBus]: :_session_bus_names'
#endif

#ifdef HAVE_FILE_TRANSFER
    '--cat=-[print content of file from sandbox container name|pid]: :_all_firejails'
    '--get=-[get a file from sandbox container name|pid]: :_all_firejails'
    # --put=name|pid src-filename dest-filename - put a file in sandbox container.
    '--put=-[put a file in sandbox container]: :'
    '--ls=-[list files in sandbox container name|pid]: :_all_firejails'
#endif

#ifdef HAVE_NETWORK
    '--bandwidth=-[set bandwidth limits name|pid]: :_all_firejails'
    '--defaultgw=[configure default gateway]: :'
    '--dns.print=-[print DNS configuration name|pid]: :_all_firejails'
    '--join-network=-[join the network namespace name|pid]: :_all_firejails'
    '--mac=-[set interface MAC address]: :(xx\:xx\:xx\:xx\:xx\:xx)'
    '--mtu=-[set interface MTU]: :'
    '--net=-[enable network namespaces and connect to this bridge or Ethernet interface (or none to disable)]: :->net_or_none'
    '--net.print=-[print network interface configuration name|pid]: :_all_firejails'
    '--netfilter=-[enable firewall]: :'
    '--netfilter.print=-[print the firewall name|pid]: :_all_firejails'
    '--netfilter6=-[enable IPv6 firewall]: :'
    '--netfilter6.print=-[print the IPv6 firewall name|pid]: :_all_firejails'
    '--netmask=-[define a network mask when dealing with unconfigured parent interfaces]: :'
    '--netns=-[Run the program in a named, persistent network namespace]: :'
    '--netstats[monitor network statistics]'
    '--interface=-[move interface in sandbox]: :'
    '--ip=-[set interface IP address none|dhcp|ADDRESS]: :(none dhcp)'
    '--ip6=-[set interface IPv6 address or use dhcp via dhclient]: :(dhcp)'
    '--iprange=-[configure an IP address in this range]: :'
    '--scan[ARP-scan all the networks from inside a network namespace]'
    '--veth-name=-[use this name for the interface connected to the bridge]: :'
#endif

#ifdef HAVE_OUTPUT
    '--output=-[stdout logging and log rotation]: :_files'
    '--output-stderr=-[stdout and stderr logging and log rotation]: :_files'
#endif

#ifdef HAVE_OVERLAYFS
    '(--chroot --noroot)--overlay[mount a filesystem overlay on top of the current filesystem]'
    '--overlay-clean[clean all overlays stored in $HOME/.firejail directory]'
    '(--chroot --noroot)--overlay-named=-[mount a filesystem overlay on top of the current filesystem, and store it in name directory]: :_files -/'
    '(--chroot --noroot)--overlay-tmpfs[mount a temporary filesystem overlay on top of the current filesystem]'
#endif

#ifdef HAVE_PRIVATE_HOME
    '--private-home=-[build a new user home in a temporary filesystem, and copy the files and directories in the list in the new home]: :_files'
#endif

#ifdef HAVE_USERNS
    '(--chroot --overlay --overlay-named --overlay-tmpfs)--noroot[install a user namespace with only the current user]'
#endif

#ifdef HAVE_USERTMPFS
    '--private-cache[temporary ~/.cache directory]'
    '*--tmpfs=-[mount a tmpfs filesystem on directory dirname]: :_files -/'
#endif

    '*--nowhitelist=-[disable whitelist for file or directory]: :_files'
    '*--whitelist=-[whitelist directory or file]: :_files'

#ifdef HAVE_X11
    '--x11[enable X11 sandboxing. The software checks first if Xpra is installed, then it checks if Xephyr is installed. If all fails, it will attempt to use X11 security extension]'
    '--x11=-[disable or enable specific X11 server]: :(none xephyr xorg xpra xvfb)'
    '--xephyr-extra-params=-[set Xephyr command server extra parameters for --x11=xephyr]: :(OPTIONS)'
    '--xephyr-screen=-[set screen size for --x11=xephyr]: :(WIDTHxHEIGHT)'
#endif
)


_firejail() {
    _arguments -S $_firejail_args
    case "$state" in
        cpus)
            _values -s "," 'cpus' $(_all_cpus)
            ;;
        net_or_none)
            local netdevs=($(ip link | awk '{print $2}' | grep '^.*:$' | tr -d ':'))
            local net_and_none=(none $netdevs)
            _values 'net' $net_and_none
            ;;
        seccomp)
            # TODO: syscall groups
            _values -s "," 'syscalls' $(firejail --debug-syscalls | cut -d" " -f2)
            ;;
    esac
}

# vim: ft=zsh sw=4 ts=4 et sts=4 ai
